/*
    James McQueen, Tyson Nottingham, Michael Pogson
    TCSS 372 Winter 2012
    SC2 Simulator
    
    debug.c
*/

#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#include <ctype.h>
#include <errno.h>
#include "debug.h"

/* Creates the CPU and starts the debug monitor.*/
int main() {
    CPUPtr cpu = cpu_initialize();
    DebugMonitorPtr db = debug_initialize(cpu);
    clearScreen();
    debug_start(db);
    
    return 0;
}

/* Debug monitor constructor.*/
DebugMonitorPtr debug_initialize(CPUPtr cpu) {
    DebugMonitorPtr d = malloc(sizeof(*d));
    
    debug_clearBreakPoints(d);
    
    d->cpu = cpu;
    d->rf = cpu_getRegisterFile(cpu);
    d->mm = cpu_getMemoryModule(cpu);
    d->memDumpStart = MEMORY_START_ADDRESS;
    
    d->oBufPosition = 0;
    d->oBufCapacity = DEFAULT_OBUF_SIZE;
    d->oBuf = malloc(sizeof(*d->oBuf) * d->oBufCapacity);
    d->oBuf[0] = '\0';

    return d;
}

/* Removes all breakpoints.*/
void debug_clearBreakPoints(DebugMonitorPtr db) {
    int i;
    for (i = 0; i < MAX_BREAK_POINTS; ++i) {
        db->breakPoints[i] = 0;
    }
}

/* Starts the debug monitor.*/
void debug_start(DebugMonitorPtr db) {
    int option;
    do {
        printBanner("SC-2 Simulator");
        debug_drawDataDisplay(db);
        option = debug_showMenu(db, mainMenuStr, LOAD, QUIT);
        debug_processMainMenu(db, option);
    } while (option != QUIT);
}

/* Displays the simulated computer's memory and registers.*/
void debug_drawDataDisplay(DebugMonitorPtr db) {
    printf("\n\nMemory                 Registers\n");
    debug_getSpRegValues(db);
    
    int i;
    for (i = 0; i < REGISTER_COUNT; ++i) {
        uchar mhigh = memory_getByte(db->mm, db->memDumpStart + 2 * i, &error);
        uchar mlow = memory_getByte(db->mm, db->memDumpStart + 2 * i + 1, &error);
        uchar rhigh = registerFile_getHighByte(db->rf, i, &error);
        uchar rlow = registerFile_getLowByte(db->rf, i, &error);
        
        printf("0x%04x:  %02x  %02x        $R%x:  %02x  %02x",
                db->memDumpStart + 2 * i, mhigh, mlow, i, rhigh, rlow);
        
        if (i < SP_REG_NUM) {
            printf("        %s  %04hx", spRegStrings[i], db->spRegValues[i]);
        } else if (i < SP_REG_NUM + CC_NUM) {
            printf("        %s  %i", spRegStrings[i], db->spRegValues[i]);
        }
        
        putchar('\n');
    }
}

/* Fills the debug monitor's array with the values of the CPU's special purpose
   registers. This is a convenience for the debug_drawDisplay() function
   implementation.*/
void debug_getSpRegValues(DebugMonitorPtr db) {
    db->spRegValues[PC] = cpu_getPC(db->cpu);
    db->spRegValues[IR] = cpu_getIR(db->cpu);
    db->spRegValues[MAR] = cpu_getMAR(db->cpu);
    db->spRegValues[MDR] = cpu_getMDR(db->cpu);
    db->spRegValues[SW] = cpu_getSW(db->cpu);
    db->spRegValues[N_FLAG] = cpu_getN(db->cpu);
    db->spRegValues[Z_FLAG] = cpu_getZ(db->cpu);
    db->spRegValues[C_FLAG] = cpu_getC(db->cpu);
    db->spRegValues[O_FLAG] = cpu_getO(db->cpu);
}

/* Displays a menu and gets a choice from the user. The options parameter is a
   string that shows the menu options. Min and max are the numerical range
   of choices for the options. Each choice is associated with a single digit
   number.*/
int debug_showMenu(DebugMonitorPtr db, char* options, int min, int max) {
    int c;
    do {
        printf("\n%s : ", options);
        c = getchar() - '0';
        while (getchar() != '\n') ; // Remove any extra input from stdin.
        
        if (c < min || max < c) {
            printf("Invalid choice. Please try again.\n");
        }
    } while (c < min || max < c);
    
    return c;
}

/* Displays the menu for setting register values and gets the register choice
   from the user.*/
int debug_showRegisterMenu(DebugMonitorPtr db) {
    int c;
    do {
        printf("\n%s : ", registerMenuStr);
        c = toupper(getchar());
        while (getchar() != '\n') ; // Remove any extra input from stdin.
        
        if ('0' <= c && c <= '9') {
            c = c - '0';
        } else if ('A' <= c && c <= 'F') {
            c = 10 + c - 'A';
        }
        
        if ((c < 0 || REGISTER_COUNT <= c) && (c != SET_PC && c != SET_SW)) {
            printf("Invalid choice. Please try again.\n");
        }
    } while ((c < 0 || REGISTER_COUNT <= c) && (c != SET_PC && c != SET_SW));
    
    return c;
}

/* Processes the user's choice of the main menu options.*/
void debug_processMainMenu(DebugMonitorPtr db, int option) {
    switch (option) {
        case LOAD: debug_load(db); clearScreen(); break;
        case STEP: clearScreen(); debug_step(db); break;
        case RUN: clearScreen(); debug_run(db); break;
        case SET_REGISTER:
            option = debug_showRegisterMenu(db);
            debug_processRegisterMenu(db, option);
            clearScreen();
            break;
        case MEMORY:
            option = debug_showMenu(db, memoryMenuStr, SET_MEMORY, SCROLL_DOWN);
            debug_processMemoryMenu(db, option);
            clearScreen();
            break;
        default: break;
    }
}

/* Processes the user's choice of the register menu options.*/
void debug_processRegisterMenu(DebugMonitorPtr db, int option) {
    ushort data = debug_getData(db, "Enter data in hexadecimal: ");
    if (0 <= option && option < REGISTER_COUNT) {
        registerFile_putWord(db->rf, option, data);
    } else if (option == SET_PC) {
        cpu_setPC(db->cpu, data);
    } else if (option == SET_SW) {
        cpu_setSW(db->cpu, data);
    }
}

/* Processes the user's choice of the memory menu options.*/
void debug_processMemoryMenu(DebugMonitorPtr db, int option) {
    switch (option) {
        case SET_MEMORY: debug_setMemory(db); break;
        case SHOW_BREAKPOINTS: debug_showBreakPoints(db); break;
        case TOGGLE_BREAKPOINT: debug_toggleBreakPointDialog(db); break;
        case SCROLL_UP:
            if (db->memDumpStart < MEMORY_START_ADDRESS + 2 * WORDS_PER_PAGE) {
                db->memDumpStart = MEMORY_START_ADDRESS;
            } else {
                db->memDumpStart -= 2 * WORDS_PER_PAGE;
            }
            break;
        case SCROLL_DOWN:
            if (db->memDumpStart > MEMORY_SPACE - 1 - 2 * WORDS_PER_PAGE) {
                db->memDumpStart = MEMORY_SPACE - 1 - 2 * WORDS_PER_PAGE;
            } else {
                db->memDumpStart += 2 * WORDS_PER_PAGE;
            }
            break;
        default: break;
    }
}

/* Gets an unsigned short in from the user, given a prompt. The user input must
   be in hexadecimal format.*/
ushort debug_getData(DebugMonitorPtr db, char* prompt) {
    char dataString[MAX_INPUT_LEN];
    unsigned long data;
    int valid = FALSE;
    
    do {
        printf("\n%s", prompt);
        if (fgets(dataString, MAX_INPUT_LEN, stdin)) {
            errno = 0;
            data = strtoul(dataString, (char**) NULL, 16);
            if (data > USHRT_MAX || errno) {
                printf("Invalid data. Please try again.\n\n");
            } else {
                valid = TRUE;
            }
        } else {
            printf("Invalid data. Please try again.\n\n");
        }
    } while (valid == FALSE);
    
    return data;
}

/* Sets a memory address by getting the address and the byte to place in that
   address from the user.*/
void debug_setMemory(DebugMonitorPtr db) {
    ushort address;
    
    do {
        address = debug_getData(db, "Enter byte address in hexadecimal: ");
        
        if (address < MEMORY_START_ADDRESS) {
            printf("Illegal address. Please try again.\n");
        }
        
    } while (address < MEMORY_START_ADDRESS);
    
    ushort data;  
    while ((data = debug_getData(db, "Enter byte in hexadecimal: ")) > 0xFF) {
        printf("Data entered is too large. Please try again.\n");
    }
    
    memory_putByte(db->mm, address, data);
}

/* Displays the breakpoints set by the user.*/
void debug_showBreakPoints(DebugMonitorPtr db) {
    printf("\nBreakpoints set at:\n");
    int breakPointSet = FALSE;
    
    int i;
    for (i = 0; i < MAX_BREAK_POINTS && db->breakPoints[i] != 0; ++i) {
        if (db->breakPoints[i] != 0) {
            printf("\t%x\n", db->breakPoints[i]);
            breakPointSet = TRUE;
        }
    }
    
    if (!breakPointSet) {
        printf("No breakpoints exist at this time.\n");
    }
    
    printf("\nPress enter to continue.\n");
    while (getchar() != '\n') ;
}

/* Toggles a breakpoint by getting the address of the breakpoint from the user.*/
void debug_toggleBreakPointDialog(DebugMonitorPtr db) {
    ushort address;
    
    do {
        address = debug_getData(db, "Enter breakpoint address: ");
        
        if (address < MEMORY_START_ADDRESS) {
            printf("Illegal address. Please try again.\n");
        }
        
    } while (address < MEMORY_START_ADDRESS);

    debug_toggleBreakPoint(db, address);
}

/* Toggles the breakpoint at the specified address. If the maximum breakpoints
   have been reached, a new breakpoint will not be set.*/
void debug_toggleBreakPoint(DebugMonitorPtr db, ushort address) {
    int i;
    for (i = 0; i < MAX_BREAK_POINTS; ++i) {
        if (db->breakPoints[i] == address) {
            db->breakPoints[i] = 0;
            printf("Breakpoint disabled at %04x. Press enter to continue.\n", address);
            while (getchar() != '\n') ;
            return;
        } else if (db->breakPoints[i] == 0) {
            db->breakPoints[i] = address;
            printf("Breakpoint enabled at %04x. Press enter to continue.\n", address);
            while (getchar() != '\n') ;
            return;
        }
    }
    
    printf("Breakpoint not found or maximum breakpoints enabled.  Press enter to continue.\n");
    while (getchar() != '\n') ;
}

/* Requests the name of the file of the program the user wants to debug, then
   loads the program into the simulated computer's memory.*/
void debug_load(DebugMonitorPtr db) {
    db->oBuf[0] = '\0';
    db->oBufPosition = 0;
    debug_clearBreakPoints(db);
    
    printf("\nEnter SC-2 hex file name: ");
    
    // Get file name.
    char fileName[MAX_INPUT_LEN];
    while (!fgets(fileName, MAX_INPUT_LEN, stdin)) {
        printf("Problem reading input. Please try again.\n");
    }
    
    // Remove newline if present.
    char *nl;
    nl = strchr(fileName, '\n');
    if (nl) {
        *nl = '\0';
    }
    
    // Load file.
    if ((error = debug_loadFile(db, fileName)) != NO_ERROR) {
        if (error == FILE_FORMAT_ERROR) {
            printf("File format error. Load terminated.\n");
        } else if (error == OUT_OF_BOUNDS_ERROR) {
            printf("Memory access violation in specified program. Load terminated.\n");
        } else if (error == FILE_NOT_FOUND_ERROR) {
            printf("File not found.\n");
        }
        printf("Press enter to continue.\n");
        while (getchar() != '\n') ;
    } else {
        cpu_reset(db->cpu);
    }
}

/* Loads the program from the file with the specified name into the simulated
   computer's memory.*/
int debug_loadFile(DebugMonitorPtr db, char* file_name) {
    FILE* f = fopen(file_name, "r");
    
    if (f) {
        short word;
        if (fscanf(f, "%hx", &word) != 1) {
            return FILE_FORMAT_ERROR;
        } else if (word < MEMORY_START_ADDRESS) {
            return OUT_OF_BOUNDS_ERROR;
        }
        
        cpu_setPC(db->cpu, word);
        db->memDumpStart = word;
        
        int addr = word;
        int numRead;
        while ((numRead = fscanf(f, "%hx", &word)) != EOF) {
            if (numRead != 1) {
                return FILE_FORMAT_ERROR;
            }
            
            if (memory_putWord(db->mm, addr, word) != NO_ERROR) {
                return OUT_OF_BOUNDS_ERROR;
            }
            
            addr += 2;
        }
        
        fclose(f);
    } else {
        return FILE_NOT_FOUND_ERROR;
    }
    
    return NO_ERROR;
}

/* Causes one CPU instruction cycle to be executed.*/
void debug_step(DebugMonitorPtr db) {
    if (cpu_getState(db->cpu) != STOP) {
        printBanner("Program Video Start");
        printf("\n%s\n", db->oBuf);
        printBanner("Program Video End");
        
        printf("\n\nProgram Keyboard Input>");
        
        error = cpu_instructionCycle(db->cpu);
        
        if (cpu_isVideoWaiting(db->cpu)) {
            debug_updateVideo(db);
        }
        
        if (debug_isBreakPoint(db, cpu_getPC(db->cpu))) {
            printf("Breakpoint reached at address %04x.", cpu_getPC(db->cpu));
        }
        
        printf("\n\n");
        
        if (error != NO_ERROR) {
            if (error == INVALID_INSTRUCTION_ERROR) {
                printf("Invalid instruction in loaded program.\n");
            } else if (error == OUT_OF_BOUNDS_ERROR) {
                printf("Memory access violation in loaded program.\n");
            }
            
            printf("It is recommended that you correct your program before debugging further.\n");
            printf("Press enter to continue.\n");
            while (getchar() != '\n') ;
            clearScreen();
        }
    } else {
        printf("\nProgram not loaded or CPU in halted state. Please load a program.\n");
        printf("Press enter to continue.\n");
        while (getchar() != '\n') ;
        clearScreen();
    }
}

/* Executes CPU instructions until a breakpoint or halt instruction is
   encountered.*/
void debug_run(DebugMonitorPtr db) {
    if (cpu_getState(db->cpu) != STOP) {
        while (cpu_getState(db->cpu) != STOP &&
               !debug_isBreakPoint(db, cpu_getPC(db->cpu))) {
            printf("\n\n\n\n\n");
            debug_step(db);
        }
    } else {
        printf("\nProgram not loaded or CPU in halted state. Please load a program.\n");
        printf("Press enter to continue.\n");
        while (getchar() != '\n') ;
        clearScreen();
    }
}

/* Appends to the debug monitor's program output buffer the character
   most recently written to the CPU's video device.*/
void debug_updateVideo(DebugMonitorPtr db) {
    if (db->oBufPosition >= db->oBufCapacity - 1) { // -1 is for '\0'
        int newCapacity = 2 * db->oBufCapacity;
        char* newBuf = malloc(newCapacity);
        
        int i;
        for (i = 0; i < db->oBufCapacity; ++i) {
            newBuf[i] = db->oBuf[i];
        }
        
        free(db->oBuf);
        db->oBuf = newBuf;
        db->oBufCapacity = newCapacity;
    }
    
    db->oBuf[db->oBufPosition++] = cpu_getVideo(db->cpu);;
    db->oBuf[db->oBufPosition] = '\0';
}

/* Returns whether or not the specified address is a breakpoint.*/
int debug_isBreakPoint(DebugMonitorPtr db, ushort address) {
    int i;
    for (i = 0; i < MAX_BREAK_POINTS; ++i) {
        if (address == db->breakPoints[i]) {
            return TRUE;
        }
    }
    
    return FALSE;
}

/* Clears the screen.*/
void clearScreen() {
    int i;
    for (i = 0; i < 96; ++i) {
        putchar('\n');
    }
}
